﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

using Autofac;

using Cookbook.Common;
using Cookbook.Services.Core.Commands;
using Cookbook.Services.Core.Common;
using Cookbook.Services.Core.Domain;
using Cookbook.Services.Core.ExceptionHandling;
using Cookbook.Services.Core.Handlers;
using Cookbook.Services.Core.Persistency;

namespace Cookbook.Services.Core.Commanding
{
    public class BatchedCommandExecutor
    {
        #region Private Definitions

        private static CommandFormatter commandFormatter;
        private readonly ILifetimeScope lifetimeScope;
        private readonly Func<CookbookUnitOfWork> unitOfWorkFactory;
        private readonly ILogger logger;

        #endregion

        public BatchedCommandExecutor(ILifetimeScope lifetimeScope, ILogger logger, Func<CookbookUnitOfWork> unitOfWorkFactory)
        {
            this.lifetimeScope = lifetimeScope;
            this.logger = logger;
            this.unitOfWorkFactory = unitOfWorkFactory;
        }

        /// <summary>
        ///   Buildup each command with the unityContainer and execute the command
        /// </summary>
        /// <param name = "commands">The commands.</param>
        public string Execute(params Command[] commands)
        {
            using (var uow = unitOfWorkFactory())
            {
                ExecuteCommands(commands);

                uow.SubmitChanges();

                return "";
            }
        }

        private void ExecuteCommands(IEnumerable<Command> commands)
        {
            IgnoreVersionForSuccessiveCommands(commands);

            using (var ctx = ValidationContext.CreateOrReuse())
            {
                foreach (var command in commands)
                {
                    Execute(command);
                }

                ctx.ThrowIfAny();
            }
        }

        private static void IgnoreVersionForSuccessiveCommands(IEnumerable<Command> commands)
        {
            foreach (var command in commands)
            {
                var updateCommand = command as VersionedCommand;
                if (updateCommand != null)
                {
                    updateCommand.Version = VersionedEntity.IgnoredVersion;
                }
            }
        }

        private void Execute(Command command)
        {
            commandFormatter = new CommandFormatter();
            logger.Verbose("Executing " + commandFormatter.Format(command));

            object handler = GetHandlerFor(command);
            InvokeHandler(command, handler);
        }

        private object GetHandlerFor(Command command)
        {
            Type interfaceType = typeof (IHandles<>).MakeGenericType(command.GetType());
            var handler = lifetimeScope.ResolveOptional(interfaceType);

            if (handler == null)
            {
                if (command is VersionedCommand)
                {
                    handler = lifetimeScope.Resolve<AttributeMappedCommandHandler>();
                }
                else
                {
                    throw new InvalidOperationException("Don't know how to execute command " + command.GetType().Name);
                }
            }

            return handler;
        }

        private void InvokeHandler(Command command, object handler)
        {
            try
            {
                MethodInfo methodInfo = handler.GetType().GetMethod("Handle");
                methodInfo.Invoke(handler, new[] {command});
            }
            catch (TargetInvocationException invocationException)
            {
                throw invocationException.Unwrap();
            }
        }
    }
}